0X00 前言

最近想写一点关于 Java 的东西,然后又遇见了 Java 中比较核心的几个技术,这几个技术贯穿整个 Java 的学习,也是后面各种框架的技术基础,因此深入理解这几种技术对我们后期分析各种框架是非常有帮助的,如果学的不精、不透,那么后面你分析各种漏洞的时候都会被绕进去,这也就是我对这些技术进行简单整理的原因。

0X01 泛型

1.什么是泛型

泛型,即“参数化类型”,将类型定义成参数形式(形参),使用的使用传入具体的类型(实参),也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

2.泛型解决什么样的问题

List arrayList = new ArrayList();
arrayList.add("aaaa");
arrayList.add(100);

for(int i = 0; i< arrayList.size();i++){
    String item = (String)arrayList.get(i);
    Log.d("泛型测试","item = " + item);
}

这段代码是可以编译成功的,因为 arrayList 本身就可以存放任意类型的数据,但是运行时会报错,因为你使用的时候强制将 Integer 当做 String 类型使用,所以这样的特性并不利于我们代码的纠错与维护,我们希望在编译的时候就能给我们提示,于是泛型就应运而生

3.泛型的生命周期

为了更好地使用泛型,我们首先要知道泛型的生命周期只在编译阶段有效,也就是说泛型是提供给Javac编译器看的,可以限定集合中的输入类型,让编译器挡住源程序中的非法输入,在正确检验泛型结果后,会将泛型的相关信息抹去。

代码实例:

ArrayList<String> collection2 = new ArrayList<String>();
ArrayList<Integer> collection3 = new ArrayList<Integer>();

//对于参数化的泛型类型,getClass()方法的返回值和原始类型完全一样
System.out.println(collection3.getClass());//结果为:java.util.ArrayList
System.out.println(collection3.getClass() == collection2.getClass());//结果为true

也就是说我们能绕过编译器的眼睛,来往 arrayList 里面添加其他类型数据

代码实例:

//使用反射得到集合,然后调用add方法往原本只能存储Integer对象的集合中存储一个String类型的对象
collection3.getClass().getMethod("add", Object.class).invoke(collection3, "abc");
System.out.println(collection3.get(0));//输出的结果为:abc,这证明字符串对象确实是存储到了原本只能存储Integer对象的集合中

4.泛型的使用

(1)泛型类

泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。

基本格式:

class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{
  private 泛型标识 /*(成员变量类型)*/ var; 
  .....

  }
}

代码实例:

Generic.java

//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{
    //key这个成员变量的类型为T,T的类型由外部指定
    private T key;

    public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
        this.key = key;
    }

    public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定
        return key;
    }
}

Main.java

import java.lang.*;

public class Main {

    public static void main(String[] args) {

        //泛型的类型参数只能是类类型(包括自定义类),不能是简单类型
        //传入的实参类型需与泛型的类型参数类型相同,即为Integer.
        Generic<Integer> genericInteger = new Generic<Integer>(123456);
        //传入的实参类型需与泛型的类型参数类型相同,即为String.
        Generic<String> genericString = new Generic<String>("key_vlaue");
        //不传入任何的泛型类型参数
        Generic generic = new Generic("111111");
        Generic generic1 = new Generic(4444);
        Generic generic2 = new Generic(55.55);
        Generic generic3 = new Generic(false);

        System.out.println("泛型测试 key is " + genericInteger.getKey());
        System.out.println("泛型测试 key is " + genericString.getKey());
        System.out.println("泛型测试 key is " + generic.getKey());
        System.out.println("泛型测试 key is " + generic1.getKey());
        System.out.println("泛型测试 key is " + generic2.getKey());
        System.out.println("泛型测试 key is " + generic3.getKey());


    }
}

输出结果:

泛型测试 key is 123456
泛型测试 key is key_vlaue
泛型测试 key is 111111
泛型测试 key is 4444
泛型测试 key is 55.55
泛型测试 key is false

从上面的例子我们可以看到,定义的泛型类,并不一定要传入泛型类型实参,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。但是实际上这样是不推荐的,因为编辑器会有一个警告,如下图所示

此处输入图片的描述

一些补充:

(1)参数化类型与原始类型的兼容性:

  • 参数化类型可以引用一个原始类型的对象,编译时编译器会报警告,例如:Collection<String> c = new Vector();

  • 原始类型可以引用一个参数化类型的对象,编译时编译器会报警告,例如:Collection c = new Vector<String>();

  • 参数化类型不考虑类型参数的继承关系:
    Vector<String> v = new Vector<Object>();//错误,语法上不通过
    Vector<Object> v = new Vector<String>();//错误,语法上不通过

  • 假设 Vector<String> v = new Vector<Object>;可以的话,那么以后从v中取出的对象当作String用,而v实际指向的集合中可以加入任意类型的对象,

  • 假设 Vector<Object> v = new Vector<String>;可以的话,那么以后可以向v中加入任意类型的对象,而v实际指向的集合中只能装String类型的对象

(2)思考:下面的代码会报错吗?(不会报错)

Vector v1 = new Vector<String>();//参数化类型的对象可以给原始类型的引用
Vector<Object> v=v1;//参数化类型的引用可以指向原始类型的对象

(2)泛型接口

泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中,可以看一个例子:

//定义一个泛型接口
public interface Generator<T> {
    public T next();
}

当实现泛型接口的类,未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中

即:class FruitGenerator<T> implements Generator{…}如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:”Unknown class”

class FruitGenerator<T> implements Generator<T>{
    @Override
    public T next() {
        return null;
    }
}

当实现泛型接口的类,传入泛型实参时:定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T>但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型,即:Generator<T>,public T next();中的的T都要替换成传入的String类型。

public class FruitGenerator implements Generator<String> {

    private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

    @Override
    public String next() {
        Random rand = new Random();
        return fruits[rand.nextInt(3)];
    }
}

(3)泛型通配符”?”

首先我们要明确一点,那就是泛型之间是不存在类型兼容性的,比如说,Ingeter是Number的一个子类,但是在使用 Generic<Number>作为形参的方法中,是不能使用Generic<Ingeter>的实例传入的,换句话说 Generic<Integer> 不能被看作为Generic<Number>的子类

代码实例:

public void showKeyValue1(Generic<Number> obj){
    System.out.println("泛型测试 key value is " + obj.getKey());
}

Generic<Integer> gInteger = new Generic<Integer>(123);
Generic<Number> gNumber = new Generic<Number>(456);

showKeyValue(gInteger);

showKeyValue这个方法编译器会为我们报错:Generic<java.lang.Integer> cannot be applied to Generic<java.lang.Number> showKeyValue(gInteger);

因此我们需要一个在逻辑上可以表示同时是 Generic<Integer>Generic<Number> 父类的引用类型,我们对上面的方法进行如下修改

public void showKeyValue1(Generic<?> obj){
    System.out.println("泛型测试 key value is " + obj.getKey());
}

注意:

1.这里的 “?” 代替具体的类型实参,和Number、String、Integer一样都是一种实际的类型,可以把”?看成所有类型的父类。

2.可以解决当具体类型不确定的时候,这个通配符就是 “?” 当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能。那么可以用 “?” 通配符来表未知类型。

(4)泛型方法

1.类比泛型类解释泛型方法

泛型类,是在实例化类的时候指明泛型的具体类型,而泛型方法,是在调用方法的时候指明泛型的具体类型。

实例代码:

public <T> T genericMethod(Class<T> tClass)throws InstantiationException ,
  IllegalAccessException{
        T instance = tClass.newInstance();
        return instance;
}

Object obj = genericMethod(Class.forName("com.test.test"));

解释:

1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
3)表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。

2.泛型方法的基本使用
public class GenericTest {
   //这个类是个泛型类,在上面已经介绍过
   public class Generic<T>{     
        private T key;

        public Generic(T key) {
            this.key = key;
        }

        //我想说的其实是这个,虽然在方法中使用了泛型,但是这并不是一个泛型方法。
        //这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。
        //所以在这个方法中才可以继续使用 T 这个泛型。
        public T getKey(){
            return key;
        }

        /**
         * 这个方法显然是有问题的,在编译器会给我们提示这样的错误信息"cannot reslove symbol E"
         * 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。
        public E setKey(E key){
             this.key = keu
        }
        */
    }

        /** 
         * 这才是一个真正的泛型方法。
         * 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
         * 这个T可以出现在这个泛型方法的任意位置.
         * 泛型的数量也可以为任意多个 
         *    如:public <T,K> K showKeyName(Generic<T> container){
         *        ...
         *        }
         */
        public <T> T showKeyName(Generic<T> container){
            System.out.println("container key :" + container.getKey());
            //当然这个例子举的不太合适,只是为了说明泛型方法的特性。
            T test = container.getKey();
            return test;
        }

    }
}
3.泛型类中泛型方法的使用
public class GenericFruit {
    class Fruit{
        @Override
        public String toString() {
            return "fruit";
        }
    }

    class Apple extends Fruit{
        @Override
        public String toString() {
            return "apple";
        }
    }

    class Person{
        @Override
        public String toString() {
            return "Person";
        }
    }

    class GenerateTest<T>{
        public void show_1(T t){
            System.out.println(t.toString());
        }

        //在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
        //由于泛型方法在声明的时候会声明泛型<E>,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。
        public <E> void show_3(E t){
            System.out.println(t.toString());
        }

        //在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。
        public <T> void show_2(T t){
            System.out.println(t.toString());
        }
    }

    public static void main(String[] args) {
        Apple apple = new Apple();
        Person person = new Person();

        GenerateTest<Fruit> generateTest = new GenerateTest<Fruit>();
        //apple是Fruit的子类,所以这里可以
        generateTest.show_1(apple);
        //编译器会报错,因为泛型类型实参指定的是Fruit,而传入的实参类是Person
        //generateTest.show_1(person);

        //使用这两个方法都可以成功
        generateTest.show_2(apple);
        generateTest.show_2(person);

        //使用这两个方法也都可以成功
        generateTest.show_3(apple);
        generateTest.show_3(person);
    }
}
4.静态方法与泛型

静态方法无法访问类上定义的泛型;如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法

public class StaticGenerator<T> {
    ....
    ....
    /**
     * 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
     * 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
     * 如:public static void show(T t){..},此时编译器会提示错误信息:
          "StaticGenerator cannot be refrenced from static context"
     */
    public static <T> void show(T t){

    }
}
5.泛型的上下边界

为泛型添加上边界,即传入的类型实参必须是指定类型的子类型

public void showKeyValue1(Generic<? extends Number> obj){
    System.out.println("泛型测试 key value is " + obj.getKey());
}



Generic<String> generic1 = new Generic<String>("11111");
Generic<Integer> generic2 = new Generic<Integer>(2222);
Generic<Float> generic3 = new Generic<Float>(2.4f);
Generic<Double> generic4 = new Generic<Double>(2.56);

//showKeyValue1(generic1);//这一行代码编译器会提示错误,因为String类型并不是Number类型的子类

showKeyValue1(generic2);
showKeyValue1(generic3);
showKeyValue1(generic4);

注意:

为泛型方法添加上下边界的时候,必须在权限声明与返回值之间的<T>上添加上下边界,即在泛型声明的时候添加

//public <T> T showKeyName(Generic<T extends Number> container),编译器会报错:"Unexpected bound"
public <T extends Number> T showKeyName(Generic<T> container){
    System.out.println("container key :" + container.getKey());
    T test = container.getKey();
    return test;
6.泛型数组

也就是说下面的这个例子是不可以的:

List[] ls = new ArrayList[10];

而使用通配符创建泛型数组是可以的,如下面这个例子:

List<?>[] ls = new ArrayList<?>[10];

这样也是可以的:

List[] ls = new ArrayList[10];

0X02 动态代理

1.什么是代理

我想代理这个词对我们从事计算机行业的人来说并不陌生,我们会通过代理去访问国外的一些网站,我们在渗透时通过代理去访问内部网络…代理就是我们与目标的一个中间人,我们想访问目标就要先访问代理,然后代理再代替我们访问目标。

2.我们为什么需要代理呢

我们在网络中需要代理是因为我们可能无法直接访问对应的目标,但是这在开发中并不存在,我们不会说访问不到我们自己定义的某一个类,那我们要代理做什么?实际上,我上面说了代理其实是我们与目标的一个中间人,中间人难道只能原封不动的传递信息吗?当然不是,要不就没有中间人攻击了对不对?

我们这里设置代理实际上是想动态地给我们要访问的对象添加功能,如果我们直接调用 A 对象的 a 方法的话,那就只能运行 a 方法,但是如果我们给 A 对象添加一个代理 ,然后我们在代理类中就可以在 a 方法前面添加 b 方法,在 a 方法后面添加 c 方法,然后我们通过代理类的对象 D 去调用 a 方法的时候就能同时运行 b 和 c 方法,这就是我们添加代理的目的。

在实际的场景中经常出现在要给某个返回值添加过滤器,那么我们的代理就可以作为一个过滤器,那又有人要问了,为什么不直接在原来的类里面添加过滤器?那是因为如果只有一个类还好说,但是如果有一个“类族” 都要添加过滤器呢?对吧,我们使用代理就能实现一次编写到处使用了。

3.动态代理和静态代理的区别

本节的标题是动态代理,那么相对的肯定有静态代理,为了体现动态代理的优势,我们这里还是要介绍一下静态代理的使用方法,然后对比引出动态代理。

代理的本质是新创建了一个类,这个类要相对于原始类有了更强大的功能,那么静态代理就非常的死板,他必须要实现原始类的接口,因此相当于将原始类硬编译进去了

(1)静态代理的实现

假设,开发者写代码之前并没有写文档的习惯,现在老板要求开发人员必须要在写代码前先写文档,于是我们可以给开发人员的类添加一个代理实现这个功能

/**
 * 目标对象实现的接口
 */

public interface IDeveloper {

    public void writeCode();
}


/**
 * 目标对象实现类
 */

public class Developer implements IDeveloper{
    private String name;
    public Developer(String name){
        this.name = name;
    }
    @Override
    public void writeCode() {
        System.out.println("Developer " + name + " writes code");
    }
}

/**
 * 代理类,需要实现与目标对象相同的接口来实现目标原始的功能
 */

public class DeveloperProxy implements IDeveloper{
    private IDeveloper developer;
    public DeveloperProxy(IDeveloper developer){
        this.developer = developer;
    }
    @Override
    public void writeCode() {
        System.out.println("Write documentation...");
        this.developer.writeCode();
    }
}

/**
 * 最终调用,先去实现原始的类,然后把原始类对象传入代理类创建代理类对象
 */

public class DeveloperTest {
    public static void main(String[] args) {
        IDeveloper jerry = new Developer("Jerry");
        IDeveloper jerryProxy = new DeveloperProxy(jerry);
        jerryProxy.writeCode();

    }
}

/**
 * 运行结果
 */

Write documentation...
Developer jerry writes code
解释:

1.代理类的代码中涉及到了接口类型的对象,实现相同接口的对象可以有不同的动作,实际上这是实现多态的常用手法,下面是一个使用接口实现多态的例子

(1)定义一个接口TestFace

public interface TestFace{//定一个接口
   void make();//定义一个接口方法
}

(2)定义一个类MyClass,里面包含一个接口型变量

public class MyClass{//定义一个类
    TestFace tf;//定义一个接口型变量tf
    public MyClass(TestFace tf){//构造函数初始化接口型变量tf
       this.tf = tf;
    }
    public work(){  //函数调用
       this.tf.make();//实际的接口型类实例。
    }
}

(3)定义2个类,实现接口TestFace

public class C1 implements TestFace{//定一个类C1实现接口TestFace
    public void make(){//实现接口的方法make
       System.out.println("c1");//打印c1
    }
}
public class C2 implements TestFace{//定一个类C2实现接口TestFace
    public void make(){//实现接口的方法make
       System.out.println("c2");//打印c2
    }
}

(4)使用接口变量

TestFace tf1 = new C1();//实现一个类实例C1
TestFace tf2 = new C2();//实现一个类实例C2
MyClass mc = MyClass(tf1);//定义一个MyClass的类实例,使用tf1
MyClass mc2 = MyClass(tf2);//定义一个MyClass的类实例,使用tf1
//同一个类的work,能够实现不同的打印内容
mc.work();
mc2.work();

2.最终调用部分传入的是原始的对象

(2)静态代理方式的缺点

比如我们还想要求测试工程师在进行测试之前也要写文档,那么我们还需要从新编辑我们的代理类,添加我们测试接口

public interface ITester {
    public void doTesting();
}

public class Tester implements ITester {
    private String name;
    public Tester(String name){
        this.name = name;
    }
    @Override
    public void doTesting() {
        System.out.println("Tester " + name + " is testing code");
    }
}
public class TesterProxy implements ITester{
    private ITester tester;
    public TesterProxy(ITester tester){
        this.tester = tester;
    }
    @Override
    public void doTesting() {
        System.out.println("Tester is preparing test documentation...");
        tester.doTesting();
    }
}

由于静态代理的每次为一个新的类做代理的时候都要实现新的接口,这样这个代理类就太不通用了,代码量就大大提升,所以我们就有了一个新的代理方法,叫做动态代理,这种代理方式不需要去实现所代理的累的接口,使用起来更加方便。

(2)动态代理的实现

  /**
     * 代理类,无需实现原始类的接口
     */

public class EnginnerProxy implements InvocationHandler {
    Object obj;
    public Object bind(Object obj)
    {
        this.obj = obj;
        return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj
        .getClass().getInterfaces(), this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
    throws Throwable
    {
        System.out.println("Enginner writes document");
        Object res = method.invoke(obj, args);
        return res;
    }
}

 /**
 * 新的接口
 */


public interface ITester {
    public void doTesting();
}

  /**
 * 新的类
 */


public class Tester implements ITester {
    private String name;
    public Tester(String name){
        this.name = name;
    }
    @Override
    public void doTesting() {
        System.out.println("Tester " + name + " is testing code");
    }
}

  /**
 * 最终调用,先去实现原始的类,然后把原始类对象传入代理类创建代理类对象
 */

public class DeveloperTest {
    public static void main(String[] args) {
        IDeveloper jerry = new Developer("Jerry");
        ITester Tom = new Tester("Tom");

        IDeveloper jerryProxy = (IDeveloper) new EngineerProxy().bind(jerry);
        jerryProxy.writeCode();

        ITester TomProxy = (ITester) new EngineerProxy().bind(Tom);
        TomProxy.doTesting();

    }
}

 /**
 * 运行结果
 */

Enginner writes document
Developer Jerry writes code
Enginner writes document
Tester Tom is testing code
1.InvocationHandler

首先我们看到的是 InvocationHandler ,这是 JDK 为我们提供的一个内置接口,从名字就能看出来这是一个调用处理程序的实现接口,因为我们代理类的函数调用实现都是通过内部的一个 invok 方法实现的

2.newProxyInstance

newProxyInstance 方法用来返回一个代理对象,它的函数原型:

static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) 

我们来解释一下这三个参数:

(1)ClassLoader loader: 定义了这个新的类的加载器,我们知道每一个类在 javac 编译器编译后都会从 .java 文件转化成对应的 .class 文件(这在我们的反射机制中是比较重要的部分),加载器的作用就是将 .class 文件中的 虚拟机指令转化成对应的类的字节码

(2)Class<?>[] interfaces: 用来指明生成的代理对象要实现的接口,这里其实是通过反射的方法去获取的

(3)InvocationHandler h: 来指明这个代理对象需要完成的动作,实际上就是 invoke 函数,所以这里用 This 作为实参

3.invoke

代理对象调用任何方法都会被这个方法截获,所以这个方法是代理对类的核心方法,它有三个参数,并且这三个参数是在调用过程中自动传入的,无需认为干预

(1)proxy:代表我们要代理的原始对象

(2)method:我们通过代理对象调用的原对象的方法,这里要注意,如果原始类的方法有很多的话,这里可以使用反射的方式获取到调用的具体方法名,然后写一个判断,从而调用不同的方法

if(method.getName().equals("xxx"))

(3)args:原对象方法的参数

0X03 参考链接

https://www.cnblogs.com/jiyukai/p/6958744.html
https://cloud.tencent.com/developer/article/1185885
https://zhidao.baidu.com/question/342312672.html
https://www.cnblogs.com/xdp-gacl/p/3971367.html
https://www.cnblogs.com/xdp-gacl/p/3629723.html
https://blog.csdn.net/s10461/article/details/53941091